﻿using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;

namespace DynamicObjectLibrary
{
    /// <summary>
    /// This class contains the extension methods associated with the Type class
    /// </summary>
    static class TypeExtensionMethods
    {
        /// <summary>
        /// Determines if the child type in some way implements the parent
        /// </summary>
        /// <param name="child">The child</param>
        /// <param name="parent">The base type or interface</param>
        /// <returns>True if somewhere the child implements the parent; false otherwise</returns>
        public static bool IsChildOf(this Type child, Type parent)
        {
            if (parent != null && parent.IsInterface)
            {
                if (child.Equals(parent)) return true;
                Type[] interfaces = child.GetInterfaces();
                if (interfaces.Contains(parent)) return true;
                else return false;
            }
            else
            {
                while (child != null && !child.Equals(parent))
                    child = child.BaseType;
                if (child == null) return false;
                else return true;
            }
        }
    }

    /// <summary>
    /// The main class in discussion here; the dynamic object library. It extends a normal dictionary.
    /// </summary>
    /// <typeparam name="Key">The Key type, which should be unique across the entire set of objects</typeparam>
    /// <typeparam name="Obj">The Object type</typeparam>
    class Library<Key, Obj> : Dictionary<Key, Obj>
    {
        private const string KeyFunctionNullMsg = "The key function is null";

        /// <summary>
        /// This exception is thrown if there are multiple identical keys in the set of objects currently being examined
        /// </summary>
        public class MultipleKeyException : Exception
        {
            private const string MultipleKeyExceptionMsg = "Multiple keys were found in the assembly; see properties for more information. No changes made to library object.";

            public Obj ObjectA { get; private set; }
            public Obj ObjectB { get; private set; }

            /// <summary>
            /// This is a essentially a wrapper for an exception
            /// </summary>
            /// <param name="obj0"></param>
            /// <param name="obj1"></param>
            public MultipleKeyException(Obj obj0, Obj obj1) : base(MultipleKeyExceptionMsg)
            {
                ObjectA = obj0;
                ObjectB = obj1;
            }
        }

        /// <summary>
        /// This method loads all the children of the Obj type into the dictionary 
        /// from the specified assembly, using the key function. The optional 
        /// parameters are used to specify a constructor and parameters to the
        /// constructor, but the default is an empty constructor.
        /// </summary>
        /// <param name="ParentAssembly">The assembly to load all the children of the Obj type from</param>
        /// <param name="KeyFunction">The unique key function for all classes of the Obj type</param>
        /// <param name="ConstructorParameterTypes">Optional parameters to specify the constructor signature; default is the empty constructor</param>
        /// <param name="Parameters">Optional parameters to specify the constructor parameters; default is null</param>
        public void LoadFromAssembly(Assembly ParentAssembly, Func<Obj, Key> KeyFunction, Type[] ConstructorParameterTypes = null, Object[] Parameters = null)
        {
            //handle exceptional or unusual parameters
            if (ParentAssembly == null) return;
            if (KeyFunction == null) throw new Exception(KeyFunctionNullMsg);
            if (ConstructorParameterTypes == null) ConstructorParameterTypes = Type.EmptyTypes;
            if (ConstructorParameterTypes == Type.EmptyTypes) Parameters = null;

            //create a temporary dictionary so as not to make any permenant changes
            Dictionary<Key, Obj> tempObjs = new Dictionary<Key, Obj>();

            //retrieves all the types from the assembly
            Obj[] objs = ParentAssembly.GetTypes().
                //narrow down the selection by finding only those types that are children of Obj
                Where(x => x.IsChildOf(typeof(Obj))).
                //narrow down the selection further by finding only those types that also match the constructor signature
                Where(x => x.GetConstructor(ConstructorParameterTypes) != null).
                //create a new object of the type, and cast it to Obj
                Select(x => (Obj)x.GetConstructor(ConstructorParameterTypes).Invoke(Parameters)).
                //removes all null entries
                Where(x => x != null).
                //cast to an array, invoking the previous set of methods on an IQueryable.
                ToArray();

            //handle the problematic case of multiple keys without making permenant changes to the dictionary
            foreach (Obj o in objs)
            {
                Key k = KeyFunction(o);
                if (this.ContainsKey(k)) throw new MultipleKeyException(this[k], o);
                if (tempObjs.ContainsKey(k)) throw new MultipleKeyException(tempObjs[k], o);
                tempObjs.Add(k, o);
            }
            
            //commit the changes to the dictionary
            foreach (KeyValuePair<Key, Obj> keypair in tempObjs)
                this.Add(keypair.Key, keypair.Value);
        }

        public void LoadAll(Func<Obj, Key> KeyFunction, Type[] ConstructorParameterTypes = null, Object[] Parameters = null)
        {
            Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly a in loadedAssemblies)
                LoadFromAssembly(a, KeyFunction, ConstructorParameterTypes, Parameters);
        }
    }
}
